<?php
/**
 * Talents come from diligence, and knowledge is gained by accumulation.
 *
 * @author: 晋<657306123@qq.com>
 */

namespace Xin\HttpClient;

use Closure;
use Exception;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\TransferStats;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Xin\Pipeline\PipelineManager;
use Xin\Support\Arr;
use Xin\Support\Reflect;
use Xin\Support\Str;
use Xin\Support\XML;

/**
 * Class Client
 * @method Response|mixed get($url, $data = null, $options = [])
 * @method Response|mixed head($url, $data = null, $options = [])
 * @method Response|mixed post($url, $data = null, $options = [])
 * @method Response|mixed put($url, $data = null, $options = [])
 * @method Response|mixed delete($url, $data = null, $options = [])
 * @method Response|mixed postJSON($url, $data = null, $options = [])
 * @method Response|mixed putJSON($url, $data = null, $options = [])
 * @method PromiseInterface getAsync($url, $data = null, $options = [])
 * @method PromiseInterface postAsync($url, $data = null, $options = [])
 * @method PromiseInterface putAsync($url, $data = null, $options = [])
 * @method PromiseInterface deleteAsync($url, $data = null, $options = [])
 * @method PromiseInterface postJSONAsync($url, $data = null, $options = [])
 * @method PromiseInterface putJSONAsync($url, $data = null, $options = [])
 * @method PromiseInterface deleteJSONAsync($url, $data = null, $options = [])
 * @method $this withAllowRedirects($allowRedirects)
 * @method $this withTimeout($timeout)
 * @method $this withConnectTimeout($connectTimeout)
 * @method $this withHeaders($headers)
 * @method $this withVerify($verify)
 * @method $this withHttpErrors($httpError)
 * @method $this mergeHeaders($headers)
 * @method $this mergeAllowRedirects($allowRedirects)
 */
class HttpClient
{
	use OperationConfigures, HasInterceptors;

	/**
	 * @var \GuzzleHttp\Client
	 */
	protected $client = null;

	/**
	 * @var array
	 */
	protected $requests = [];

	/**
	 * @var static
	 */
	protected static $instance = null;

	/**
	 * @var array
	 */
	protected static $defaultOptions = [];

	/**
	 * Client constructor.
	 *
	 * @param array $options
	 * @noinspection PhpDocMissingThrowsInspection
	 */
	public function __construct(array $options = [])
	{
		$options = array_replace_recursive(self::getDefaultOptions(), $options);
		$this->client = static::createGuzzleHttpClient($options);

		$this->interceptor = new PipelineManager();

		$this->injectMiddleware();
	}

	/**
	 * 拦截器注入中间件
	 * @return void
	 */
	protected function injectMiddleware()
	{
		/** @var HandlerStack $stack */
		$stack = $this->getOptions('handler');
		$stack->push(function (callable $handler) {
			return function (RequestInterface $request, $options) use ($handler) {
				$requestId = $options['request_id'];
				$this->requests[$requestId] = $request;

				/** @var PromiseInterface $response */
				return $handler($request, $options);
			};
		}, 'interceptor');
	}

	/**
	 * 发起请求
	 *
	 * @param string $method
	 * @param string $url
	 * @param mixed $data
	 * @param array $options
	 * @return PromiseInterface|Response|mixed
	 */
	public function request(string $method, string $url, $data = null, array $options = [])
	{
		$options['request_id'] = Str::random();
		$method = strtoupper($method);

		// 处理请求Header数据
		if (!isset($options['headers'])) {
			$options['headers'] = [];
		}

		// 判断是否是 JSON 请求
		$isJsonRequest = false;
		if (Str::contains($method, "JSON")) {
			$isJsonRequest = true;
			$method = Str::before($method, "JSON");
			$options['headers']['Content-Type'] = 'application/json';
		}

		// 判断是否异步
		$isAsync = Str::endsWith($method, "ASYNC");
		if ($isAsync) {
			$method = Str::before($method, "ASYNC");
		}

		// 强制 JSON 请求
		if ($isJsonRequest) {
			$options['json'] = $data;
		} else {
			if ('POST' == $method || 'PUT' == $method) {
				// 判断 $contentType 类型
				$contentType = Arr::get($options, 'headers.Content-Type') ?: $this->getOptions('headers.Content-Type');
				$isJsonData = $contentType === 'application/json';
				$isXmlData = $contentType === 'application/xml';
				$isTextData = $contentType === 'text/plain';
				$isRawData = $isXmlData || $isTextData;

				if ($isJsonData) {
					$options['json'] = $data;
				} elseif ($isXmlData) {
					$options['body'] = is_scalar($data) ? $data : XML::encode($data);
				} elseif ($isTextData) {
					$options['body'] = is_scalar($data) ? $data : json_encode($data);
				} else {
					$options['form_params'] = $data;
				}
				unset($data);
			} elseif ('UPLOAD' == $method) {
				$method = "POST";
				$options['multipart'] = $data;
			} elseif ('DELETE' == $method) {
				$options['query'] = $data;
			} else {
				$options['query'] = $data;
			}
		}

		$options['method'] = $method;

		return $this->send($isAsync, $method, $url, $options);
	}

	/**
	 * 发送请求
	 * @param bool $isAsync
	 * @param string $method
	 * @param string $url
	 * @param array $options
	 * @return PromiseInterface|Response|mixed
	 */
	protected function send(bool $isAsync, string $method, string $url, array $options)
	{
		// 构建请求
		$options = Reflect::call($this->client, 'prepareDefaults', [$options]);
		$options = $this->invokeRequestInterceptors($options);

		// 处理统计信息
		$tmpTransferStats = null;
		$onStats = function ($transferStats) use ($options, &$tmpTransferStats) {
			if (($callback = Arr::get($options, 'on_stats')) instanceof Closure) {
				$transferStats = $callback($transferStats) ?: $transferStats;
			}
			$tmpTransferStats = $transferStats;
		};

		// 发起请求
		if ($isAsync) {
			return $this->client->requestAsync($method, $url, array_merge($options, [
				'on_stats' => $onStats,
			]))->then(function ($psrResponse) use ($options, &$tmpTransferStats) {
				$response = $this->makeResponse($options, $psrResponse, null, $tmpTransferStats);
				return $this->invokeResponseInterceptors($response);
			})->otherwise(function ($e) use ($options, &$tmpTransferStats) {
				$response = $this->makeResponse($options, null, $e, $tmpTransferStats);
				return $this->invokeResponseInterceptors($response);
			});
		} else {
			try {
				$psrResponse = $this->client->request($method, $url, array_merge($options, [
					'on_stats' => $onStats,
				]));
				$response = $this->makeResponse($options, $psrResponse, null, $tmpTransferStats);
			} catch (GuzzleException $e) {
				$response = $this->makeResponse($options, null, $e, $tmpTransferStats);
			}

			return $this->invokeResponseInterceptors($response);
		}
	}

	/**
	 * 创建响应对象
	 * @param ResponseInterface|null $psrResponse
	 * @param Exception $e
	 * @param array|null $requestInfo
	 * @param TransferStats $transferStats
	 * @return Response
	 */
	protected function makeResponse(array $options, $psrResponse, $e = null, $transferStats = null)
	{
		$requestId = $options['request_id'];
		$request = $this->pullRequest($requestId);
		if ($e) {
			$psrResponse = $e instanceof BadResponseException ? $e->getResponse() : null;
		}

		return new Response(
			$psrResponse,
			$e,
			$request,
			$options,
			$this,
			$transferStats
		);
	}

	/**
	 * Upload file.
	 *
	 * @param string $url
	 * @param array $files
	 * @param array $form
	 * @param array $query
	 * @param array $options
	 * @return Response
	 */
	public function upload(string $url, array $files = [], array $form = [], array $query = [], array $options = [])
	{
		$multipart = [];
		foreach ($files as $name => $path) {
			if (!is_array($path)) {
				$path = [
					'path' => $path,
					'filename' => basename($path),
				];
			}

			$multipart[] = array_filter([
				'name' => $name,
				'contents' => fopen($path['path'], 'r'),
				'filename' => $path['filename'],
			]);
		}
		foreach ($form as $name => $contents) {
			$multipart[] = compact('name', 'contents');
		}

		$options = array_replace_recursive($options, [
			'query' => $query,
			'connect_timeout' => 30,
			'timeout' => 300,
		]);

		return $this->request('UPLOAD', $url, $multipart, $options);
	}

	/**
	 * 获取请求信息
	 * @param string $requestId
	 * @return array|null
	 */
	private function pullRequest(string $requestId)
	{
		if (isset($this->requests[$requestId])) {
			$request = $this->requests[$requestId];
			unset($this->requests[$requestId]);
			return $request;
		}

		return null;
	}

	/**
	 * 获取正在运行的请求
	 * @param string $requestId
	 * @return RequestInterface|null
	 */
	public function getRunningRequest(string $requestId)
	{
		if (isset($this->requests[$requestId])) {
			return $this->requests[$requestId];
		}

		return null;
	}

	/**
	 * 获取 Cookies
	 * @return array
	 */
	public function cookies()
	{
		$cookieJar = $this->cookieJar();
		return $cookieJar ? $cookieJar->toArray() : [];
	}

	/**
	 * 获取默认实例
	 *
	 * @return static
	 */
	public static function instance()
	{
		if (static::$instance === null) {
			static::$instance = new static();
		}

		return static::$instance;
	}

	/**
	 * 获取默认实例
	 *
	 * @return static
	 */
	public static function default()
	{
		return static::instance();
	}

	/**
	 * 获取默认的配置
	 * @return array|int[]
	 */
	public static function getDefaultOptions()
	{
		if (empty(self::$defaultOptions)) {
			self::$defaultOptions = [
				'headers' => [
					'Accept' => 'application/json',
					'Content-Type' => 'application/json',
					'X-Requested-With' => 'XMLHttpRequest',
				],
				'timeout' => 30,
				'connect_timeout' => 5,
				// 'allow_redirects' => [
				// 	'max' => 5,
				// 	'strict' => false,
				// 	'referer' => true,
				// 	'protocols' => ['http', 'https'],
				// 	'track_redirects' => false,
				// ],
				'http_errors' => true,
				'allow_redirects' => false,
				'verify' => false, // 禁用SSL验证
			];
		}

		return self::$defaultOptions;
	}

	/**
	 * 合并配置
	 * @param array $newOptions
	 * @return void
	 */
	public static function mergeDefaultOptions(array $newOptions)
	{
		$defaultOptions = static::getDefaultOptions();
		self::$defaultOptions = array_replace_recursive($defaultOptions, $newOptions);
	}

	/**
	 * 设置新的默认配置
	 * @param array $newOptions
	 * @return void
	 */
	public static function setDefaultOptions(array $newOptions)
	{
		self::$defaultOptions = $newOptions;
	}

	/**
	 * 批量发起请求
	 * @param array $promises
	 * @return mixed
	 */
	public static function all(array $promises)
	{
		return \GuzzleHttp\Promise\Utils::all($promises)->wait();
	}

	/**
	 * 创建新的客户端
	 * @param array $newOptions
	 * @return static
	 */
	public static function create(array $newOptions = [])
	{
		return new static($newOptions);
	}

	/**
	 * 创建 GuzzleHttp Client
	 * @param array $options
	 * @return \GuzzleHttp\Client
	 */
	public static function createGuzzleHttpClient(array $options)
	{
		// 来自 hyperf 框架
		if (class_exists(\Hyperf\Guzzle\ClientFactory::class)) {
			$handlerStackFactory = new \Hyperf\Guzzle\HandlerStackFactory();
			$stack = $handlerStackFactory->create();
			$options['handler'] = $stack;

			$clientFactory = \Hyperf\Support\make(\Hyperf\Guzzle\ClientFactory::class);
			$client = $clientFactory->create($options);
		} else {
			$client = new \GuzzleHttp\Client($options);
		}

		return $client;
	}

	/**
	 * 动态方法
	 *
	 * @param string $name
	 * @param array $arguments
	 * @return false|mixed
	 */
	public function __call($name, $arguments)
	{
		if (Str::startsWith($name, 'with')) {
			$key = Str::snake(substr($name, 4));
			return $this->setOption($key, $arguments[0]);
		} elseif (Str::startsWith($name, 'merge')) {
			$key = Str::snake(substr($name, 5));
			return $this->mergeOptions([$key => $arguments[0]]);
		}

		// elseif (Str::startsWith($name, 'get')) {
		// 	$key = Str::snake(substr($name, 3));
		// 	return $this->getOptions($key);
		// }

		array_unshift($arguments, $name);
		return call_user_func_array([$this, 'request'], $arguments);
	}

	/**
	 * 静态调用动态方法
	 *
	 * @param string $name
	 * @param array $arguments
	 * @return false|mixed
	 */
	public static function __callStatic($name, $arguments)
	{
		return call_user_func_array([static::instance(), $name], $arguments);
	}

}
